/**
 * 
 */
package coop.tecso.core.test.factory.impl;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.org.usurper.handlers.IHandler;
import org.org.usurper.handlers.basic.AbstractPropertyTypeHandler;
import org.org.usurper.handlers.basic.AbstractSpecificPropertyHandler;
import org.org.usurper.setup.UsurperGeneratorSetup;

import coop.tecso.core.test.factory.GenericFactory;
import coop.tecso.core.test.factory.MultipleInstanceStrategy;
import coop.tecso.core.test.generator.UsurperGenerator;
import coop.tecso.core.test.handlers.PropertySingletonHandler;
import coop.tecso.core.test.handlers.SpecificSingletonHandler;
import coop.tecso.core.test.handlers.factory.PropertyHandlerFactory;

/**
 * @author daniel
 */
public class UsurperFactory implements GenericFactory {

    private final UsurperGeneratorSetup setup;

    private PropertyHandlerFactory propertyHandlerFactory;

    private static final Logger log = Logger.getLogger(UsurperFactory.class);

    /**
     * 
     */
    public UsurperFactory(final PropertyHandlerFactory propertyHandlerFactory) {
        setup = new UsurperGeneratorSetup();
        this.propertyHandlerFactory = propertyHandlerFactory;
    }

    /*
     * (non-Javadoc)
     * @see com.avritech.zapsaude.factory.IGenericFactory#getNewInstance()
     */
    @Override
    public <T> T getNewInstance(final Class<T> clazz) {
        return getNewInstance(clazz, new HashMap<String, Object>());
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstance(java.util
     * .Map)
     */
    @Override
    public <T> T getNewInstance(final Class<T> clazz,
            final Set<Class<?>> reusables) {
        return getNewInstance(clazz, reusables, new HashMap<String, Object>());
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstance(java.util
     * .Map)
     */
    @Override
    public <T> T getNewInstance(final Class<T> clazz,
            final Map<String, Object> overrides) {
        return getNewInstance(clazz, new HashSet<Class<?>>(), overrides);
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstance(java.lang
     * .Class, java.util.Set, java.util.Map)
     */
    @Override
    public <T> T getNewInstance(final Class<T> clazz,
            final Set<Class<?>> reuses, final Map<String, Object> overrides) {
        final UsurperGenerator<T> generator = getConfiguredGenerator(clazz,
                reuses, overrides);
        final T usurped = generator.generateUsurper();
        // TODO Callback hooks here
        return usurped;
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstances(java.lang
     * .Class, int)
     */
    @Override
    public <T> Collection<T> getNewInstances(final Class<T> clazz,
            final int number) {
        return getNewInstances(clazz, number, new HashSet<Class<?>>(),
                new DistinctInstancesStrategy());
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstances(java.lang
     * .Class, int, boolean)
     */
    @Override
    public <T> Collection<T> getNewInstances(final Class<T> clazz,
            final int number,
            final MultipleInstanceStrategy multipleInstanceStrategy) {
        return getNewInstances(clazz, number, multipleInstanceStrategy);
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstances(java.lang
     * .Class, int, java.util.Map, java.util.Map, boolean)
     */
    @Override
    public <T> Collection<T> getNewInstances(final Class<T> clazz,
            final int number, final Set<Class<?>> reuses,
            final Map<String, Object> overrides,
            final MultipleInstanceStrategy multipleInstanceStrategy) {
        final UsurperGenerator<T> generator = getConfiguredGenerator(clazz,
                reuses, overrides);
        final Collection<T> generated = multipleInstanceStrategy.getCollection(
                generator, number);
        // TODO Callback hooks here
        return generated;
    }

    /*
     * (non-Javadoc)
     * @see
     * com.avritech.zapsaude.factory.GenericFactory#getNewInstances(java.lang
     * .Class, int, java.util.Map, boolean)
     */
    @Override
    public <T> Collection<T> getNewInstances(final Class<T> clazz,
            final int number, final Set<Class<?>> reuses,
            final MultipleInstanceStrategy multipleInstanceStrategy) {
        return getNewInstances(clazz, number, reuses,
                new HashMap<String, Object>(), multipleInstanceStrategy);
    }

    /**
     * @return the propertyHandlerFactory
     */
    public PropertyHandlerFactory getPropertyHandlerFactory() {
        return propertyHandlerFactory;
    }

    private boolean isComplex(final Class<?> propertyClass) {
        return !propertyClass.isPrimitive() && !propertyClass.isArray()
                && !propertyClass.isEnum()
                && !propertyClass.getPackage().getName().startsWith("java.");
    }

    private String matchPropertyPath(final Map<String, Object> overrides,
            final String propertyPath) {
        if (overrides.containsKey(propertyPath)) {
            return propertyPath;
        }
        final Iterator<String> keys = overrides.keySet().iterator();
        while (keys.hasNext()) {
            final String nextKey = keys.next();
            if (Pattern.matches(nextKey, propertyPath)) {
                return nextKey;
            }
        }
        return null;
    }

    private void registerCommonHandlers() throws IntrospectionException {
        setup.registerPropertyTypeHandler(new coop.tecso.core.test.handlers.RandomBigDecimalPropertyHandler());
    }

    private void registerHandlersFor(final Class<?> factoryClass,
            final String propertyPath, final Set<Class<?>> reusable,
            final Map<String, Object> overrides,
            final Set<PendingProperty> yetToBeRegistered)
                    throws IntrospectionException {
        final PropertyDescriptor[] descriptors = Introspector.getBeanInfo(
                factoryClass).getPropertyDescriptors();
        for (final PropertyDescriptor descriptor : descriptors) {
            final Class<?> propertyClass = descriptor.getPropertyType();
            final String propertyName = descriptor.getName();
            final String localPropertyPath = buildPathToProperty(propertyPath,
                    propertyName);
            if (isComplex(propertyClass) || propertyClass.equals(String.class)
                    || overrides.containsKey(localPropertyPath)) {
                final IHandler actualHandler = propertyHandlerFactory.build(
                        factoryClass, descriptor);

                registerSpecificHandlersWhenAppropriate(factoryClass,
                        localPropertyPath, descriptor, reusable, overrides,
                        actualHandler);
            }
            if (isComplex(propertyClass)) {
                collectDeepProperties(yetToBeRegistered, descriptor,
                        localPropertyPath);
            }
        }
    }

    private void registerPlainHandlersFor(final Class<?> factoryClass,
            final PropertyDescriptor descriptor, final String propertyPath,
            final Set<Class<?>> reusable, final Map<String, Object> overrides) {
        final Class<?> propertyClass = descriptor.getPropertyType();
        final String propertyName = descriptor.getName();
        final String localPropertyPath = buildPathToProperty(propertyPath,
                propertyName);
        if (isComplex(propertyClass) || propertyClass.equals(String.class)
                || overrides.containsKey(localPropertyPath)) {
            final IHandler actualHandler = propertyHandlerFactory.build(
                    factoryClass, descriptor);
            registerSpecificHandlersWhenAppropriate(factoryClass,
                    localPropertyPath, descriptor, reusable, overrides,
                    actualHandler);
        }
    }

    private void registerSpecificHandlersWhenAppropriate(
            final Class<?> propertyClass, final String propertyPath,
            final PropertyDescriptor descriptor, final Set<Class<?>> reusable,
            final Map<String, Object> overrides, final IHandler actualHandler) {
        String matchedKey = "";
        if ((matchedKey = matchPropertyPath(overrides, propertyPath)) != null) {
            final AbstractSpecificPropertyHandler wrapperHandler = new SpecificSingletonHandler(
                    propertyClass, descriptor.getName(),
                    overrides.get(matchedKey));
            setup.registerSpecificPropertyHandler(wrapperHandler);
        } else if (reusable.contains(descriptor.getPropertyType())) {
            final AbstractPropertyTypeHandler wrapperHandler = new PropertySingletonHandler(
                    (AbstractPropertyTypeHandler) actualHandler);
            setup.registerPropertyTypeHandler(wrapperHandler);
        } else if (AbstractSpecificPropertyHandler.class
                .isAssignableFrom(actualHandler.getClass())) {
            setup.registerSpecificPropertyHandler((AbstractSpecificPropertyHandler) actualHandler);
        } else {
            setup.registerPropertyTypeHandler((AbstractPropertyTypeHandler) actualHandler);
        }
    }

    /**
     * @param propertyHandlerFactory
     *            the propertyHandlerFactory to set
     */
    public void setPropertyHandlerFactory(
            final PropertyHandlerFactory propertyHandlerFactory) {
        this.propertyHandlerFactory = propertyHandlerFactory;
    }

    private String buildPathToProperty(final String pathBefore,
            final String propertyName) {
        if (pathBefore.isEmpty()) {
            return propertyName;
        }
        return pathBefore + "." + propertyName;
    }

    private void collectDeepProperties(final Set<PendingProperty> pending,
            final PropertyDescriptor desc, final String propertyPath) {
        try {
            if (!pending.add(new PendingProperty(desc, propertyPath))) {
                return;
            }
            final PropertyDescriptor[] descriptors = Introspector.getBeanInfo(
                    desc.getPropertyType()).getPropertyDescriptors();
            for (final PropertyDescriptor descriptor : descriptors) {
                final Class<?> propertyClass = descriptor.getPropertyType();
                final String propertyName = descriptor.getName();
                final String localPropertyPath = buildPathToProperty(
                        propertyPath, propertyName);
                if (isComplex(propertyClass)
                        || propertyClass.equals(String.class)) {
                    collectDeepProperties(pending, descriptor,
                            localPropertyPath);
                }
            }
        } catch (final IntrospectionException e) {
            e.printStackTrace();
        }
    }

    private <T> UsurperGenerator<T> getConfiguredGenerator(
            final Class<T> clazz, final Set<Class<?>> reusables,
            final Map<String, Object> overrides) {
        try {
            final Set<PendingProperty> pendingHandlers = new HashSet<UsurperFactory.PendingProperty>();
            registerHandlersFor(clazz, "", reusables, overrides,
                    pendingHandlers);
            for (final PendingProperty pendingProperty : pendingHandlers) {
                registerPlainHandlersFor(clazz,
                        pendingProperty.getDescriptor(),
                        pendingProperty.getPropertyPath(), reusables, overrides);
            }
            registerCommonHandlers();
            final UsurperGenerator<T> generator = new UsurperGenerator<T>(
                    clazz, setup);
            return generator;
        } catch (final IntrospectionException e) {
            log.error(e, e);
        }
        return null;
    }

    class PendingProperty {
        private final PropertyDescriptor descriptor;
        private final String propertyPath;

        /**
         * @param descriptor
         * @param propertyPath
         */
        public PendingProperty(final PropertyDescriptor descriptor,
                final String propertyPath) {
            super();
            this.descriptor = descriptor;
            this.propertyPath = propertyPath;
        }

        /*
         * (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final PendingProperty other = (PendingProperty) obj;
            if (!getOuterType().equals(other.getOuterType())) {
                return false;
            }
            if (descriptor == null) {
                if (other.descriptor != null) {
                    return false;
                }
            } else if (!descriptor.equals(other.descriptor)) {
                return false;
            }
            if (propertyPath == null) {
                if (other.propertyPath != null) {
                    return false;
                }
            } else if (!propertyPath.equals(other.propertyPath)) {
                return false;
            }
            return true;
        }

        /**
         * @return the descriptor
         */
        public PropertyDescriptor getDescriptor() {
            return descriptor;
        }

        private UsurperFactory getOuterType() {
            return UsurperFactory.this;
        }

        /**
         * @return the propertyPath
         */
        public String getPropertyPath() {
            return propertyPath;
        }

        /*
         * (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result
                    + (descriptor == null ? 0 : descriptor.hashCode());
            result = prime * result
                    + (propertyPath == null ? 0 : propertyPath.hashCode());
            return result;
        }

        /*
         * (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return "[descriptor=" + descriptor + ", propertyPath="
                    + propertyPath + "]";
        }
    }
}
